@fieldwangai/agentflow 0.1.43 → 0.1.44
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/bin/lib/composer-agent.mjs +99 -6
- package/bin/lib/ui-server.mjs +202 -12
- package/builtin/nodes/display_ascii.md +1 -0
- package/builtin/nodes/display_chart.md +1 -1
- package/builtin/nodes/display_html.md +1 -1
- package/builtin/nodes/display_image.md +1 -1
- package/builtin/nodes/display_markdown.md +1 -0
- package/builtin/nodes/display_mermaid.md +1 -0
- package/builtin/nodes/display_table.md +1 -1
- package/builtin/web-ui/dist/assets/index-CcZ_lCII.js +222 -0
- package/builtin/web-ui/dist/assets/index-DLRqQLBo.css +1 -0
- package/builtin/web-ui/dist/index.html +2 -2
- package/package.json +1 -1
- package/builtin/web-ui/dist/assets/index-BCquw8dA.js +0 -218
- package/builtin/web-ui/dist/assets/index-CXO3VpkR.css +0 -1
|
@@ -50,6 +50,13 @@ function readUserMcpPrivateEnvObject(userId) {
|
|
|
50
50
|
return env;
|
|
51
51
|
}
|
|
52
52
|
|
|
53
|
+
function readUserMcpPrivateServers(userId) {
|
|
54
|
+
const safe = sanitizeAgentflowUserId(userId);
|
|
55
|
+
if (!safe) return {};
|
|
56
|
+
const data = readJsonObject(path.join(getAgentflowUserDataRoot(safe), "mcp-private.json"));
|
|
57
|
+
return data?.servers && typeof data.servers === "object" && !Array.isArray(data.servers) ? data.servers : {};
|
|
58
|
+
}
|
|
59
|
+
|
|
53
60
|
function pruneCursorMcpPrivateEnvPlaceholders() {
|
|
54
61
|
const filePath = path.join(os.homedir(), ".cursor", "mcp.json");
|
|
55
62
|
const config = readJsonObject(filePath);
|
|
@@ -83,12 +90,98 @@ function pruneCursorMcpPrivateEnvPlaceholders() {
|
|
|
83
90
|
fs.writeFileSync(filePath, JSON.stringify({ ...config, mcpServers: nextServers }, null, 2) + "\n", "utf-8");
|
|
84
91
|
}
|
|
85
92
|
|
|
93
|
+
function cursorMcpServersFromFile(filePath) {
|
|
94
|
+
const config = readJsonObject(filePath);
|
|
95
|
+
return config?.mcpServers && typeof config.mcpServers === "object" && !Array.isArray(config.mcpServers)
|
|
96
|
+
? config.mcpServers
|
|
97
|
+
: {};
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
function materializeWorkspaceCursorMcpPrivateConfig(workspaceRoot, userId) {
|
|
101
|
+
const safe = sanitizeAgentflowUserId(userId);
|
|
102
|
+
if (!safe) return () => {};
|
|
103
|
+
const privateServers = readUserMcpPrivateServers(safe);
|
|
104
|
+
if (!Object.keys(privateServers).length) return () => {};
|
|
105
|
+
|
|
106
|
+
const workspace = path.resolve(workspaceRoot || process.cwd());
|
|
107
|
+
const filePath = path.join(workspace, ".cursor", "mcp.json");
|
|
108
|
+
const globalFilePath = path.join(os.homedir(), ".cursor", "mcp.json");
|
|
109
|
+
const existed = fs.existsSync(filePath);
|
|
110
|
+
const original = existed ? fs.readFileSync(filePath, "utf-8") : "";
|
|
111
|
+
const config = readJsonObject(filePath);
|
|
112
|
+
const localServers = cursorMcpServersFromFile(filePath);
|
|
113
|
+
const globalServers = cursorMcpServersFromFile(globalFilePath);
|
|
114
|
+
const nextServers = { ...localServers };
|
|
115
|
+
let changed = false;
|
|
116
|
+
|
|
117
|
+
for (const [name, privateServer] of Object.entries(privateServers)) {
|
|
118
|
+
const current = nextServers[name] || globalServers[name];
|
|
119
|
+
if (!current || typeof current !== "object" || Array.isArray(current)) continue;
|
|
120
|
+
const privateEnv = privateServer?.env && typeof privateServer.env === "object" && !Array.isArray(privateServer.env) ? privateServer.env : {};
|
|
121
|
+
const privateHeaders = privateServer?.headers && typeof privateServer.headers === "object" && !Array.isArray(privateServer.headers) ? privateServer.headers : {};
|
|
122
|
+
if (!Object.keys(privateEnv).length && !Object.keys(privateHeaders).length) continue;
|
|
123
|
+
|
|
124
|
+
const next = { ...current };
|
|
125
|
+
if (Object.keys(privateEnv).length) {
|
|
126
|
+
const currentEnv = current.env && typeof current.env === "object" && !Array.isArray(current.env) ? current.env : {};
|
|
127
|
+
next.env = { ...currentEnv, ...privateEnv };
|
|
128
|
+
changed = true;
|
|
129
|
+
}
|
|
130
|
+
if (Object.keys(privateHeaders).length) {
|
|
131
|
+
const currentHeaders = current.headers && typeof current.headers === "object" && !Array.isArray(current.headers) ? current.headers : {};
|
|
132
|
+
next.headers = { ...currentHeaders, ...privateHeaders };
|
|
133
|
+
changed = true;
|
|
134
|
+
}
|
|
135
|
+
nextServers[name] = next;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
if (!changed) return () => {};
|
|
139
|
+
fs.mkdirSync(path.dirname(filePath), { recursive: true });
|
|
140
|
+
fs.writeFileSync(filePath, JSON.stringify({ ...config, mcpServers: nextServers }, null, 2) + "\n", "utf-8");
|
|
141
|
+
|
|
142
|
+
let restored = false;
|
|
143
|
+
return () => {
|
|
144
|
+
if (restored) return;
|
|
145
|
+
restored = true;
|
|
146
|
+
try {
|
|
147
|
+
if (existed) fs.writeFileSync(filePath, original, "utf-8");
|
|
148
|
+
else if (fs.existsSync(filePath)) fs.rmSync(filePath, { force: true });
|
|
149
|
+
} catch {
|
|
150
|
+
// Best-effort restore; do not fail an already-running agent on cleanup.
|
|
151
|
+
}
|
|
152
|
+
};
|
|
153
|
+
}
|
|
154
|
+
|
|
86
155
|
function agentflowUserEnv(userId) {
|
|
87
156
|
const safe = sanitizeAgentflowUserId(userId);
|
|
88
157
|
pruneCursorMcpPrivateEnvPlaceholders();
|
|
89
158
|
return { ...readMergedEnvObject(safe), ...(safe ? readUserMcpPrivateEnvObject(safe) : {}), AGENTFLOW_USER_ID: safe };
|
|
90
159
|
}
|
|
91
160
|
|
|
161
|
+
function runCursorAgentWithPrivateMcp(cliWorkspace, prompt, options, userId) {
|
|
162
|
+
const restore = materializeWorkspaceCursorMcpPrivateConfig(cliWorkspace, userId);
|
|
163
|
+
let handle;
|
|
164
|
+
try {
|
|
165
|
+
handle = runCursorAgentWithPrompt(cliWorkspace, prompt, options);
|
|
166
|
+
} catch (e) {
|
|
167
|
+
restore();
|
|
168
|
+
throw e;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
let restored = false;
|
|
172
|
+
const safeRestore = () => {
|
|
173
|
+
if (restored) return;
|
|
174
|
+
restored = true;
|
|
175
|
+
restore();
|
|
176
|
+
};
|
|
177
|
+
if (handle?.child?.once) {
|
|
178
|
+
handle.child.once("exit", safeRestore);
|
|
179
|
+
handle.child.once("error", safeRestore);
|
|
180
|
+
}
|
|
181
|
+
const finished = Promise.resolve(handle.finished).finally(safeRestore);
|
|
182
|
+
return { ...handle, finished };
|
|
183
|
+
}
|
|
184
|
+
|
|
92
185
|
// ─── script 内容注入辅助 ─────────────────────────────────────────────────
|
|
93
186
|
|
|
94
187
|
/**
|
|
@@ -203,10 +296,10 @@ export function startComposerAgent(opts) {
|
|
|
203
296
|
});
|
|
204
297
|
}
|
|
205
298
|
|
|
206
|
-
return
|
|
299
|
+
return runCursorAgentWithPrivateMcp(cliWs, prompt, {
|
|
207
300
|
...common,
|
|
208
301
|
model: model || undefined,
|
|
209
|
-
});
|
|
302
|
+
}, opts.agentflowUserId);
|
|
210
303
|
}
|
|
211
304
|
|
|
212
305
|
// ─── 为单个 agent 步骤构建 prompt ──────────────────────────────────────────
|
|
@@ -496,12 +589,12 @@ export async function runComposerPostFlowValidationAndRepair(opts) {
|
|
|
496
589
|
setChild(handle.child);
|
|
497
590
|
await handle.finished;
|
|
498
591
|
} else {
|
|
499
|
-
const handle =
|
|
592
|
+
const handle = runCursorAgentWithPrivateMcp(cliWs, agentPrompt, {
|
|
500
593
|
onStreamEvent: stepEmit,
|
|
501
594
|
model: model || undefined,
|
|
502
595
|
force: Boolean(opts.force),
|
|
503
596
|
env,
|
|
504
|
-
});
|
|
597
|
+
}, opts.agentflowUserId || opts.flowContext?.userId);
|
|
505
598
|
setChild(handle.child);
|
|
506
599
|
await handle.finished;
|
|
507
600
|
}
|
|
@@ -769,12 +862,12 @@ export function startComposerMultiStep(opts) {
|
|
|
769
862
|
currentChild = handle.child;
|
|
770
863
|
await handle.finished;
|
|
771
864
|
} else {
|
|
772
|
-
const handle =
|
|
865
|
+
const handle = runCursorAgentWithPrivateMcp(cliWs, agentPrompt, {
|
|
773
866
|
onStreamEvent: stepEmit,
|
|
774
867
|
model: model || undefined,
|
|
775
868
|
force: Boolean(opts.force),
|
|
776
869
|
env,
|
|
777
|
-
});
|
|
870
|
+
}, opts.agentflowUserId || opts.flowContext?.userId);
|
|
778
871
|
currentChild = handle.child;
|
|
779
872
|
await handle.finished;
|
|
780
873
|
}
|
package/bin/lib/ui-server.mjs
CHANGED
|
@@ -1177,6 +1177,32 @@ function uniqueWorkspaceRelPath(workspaceRoot, relPath) {
|
|
|
1177
1177
|
return { abs, rel };
|
|
1178
1178
|
}
|
|
1179
1179
|
|
|
1180
|
+
function workspaceDownloadContentDisposition(relPath) {
|
|
1181
|
+
const fallbackName = path.basename(String(relPath || "download")) || "download";
|
|
1182
|
+
const quotedName = fallbackName.replace(/[\r\n"\\]/g, "_");
|
|
1183
|
+
return `attachment; filename="${quotedName}"; filename*=UTF-8''${encodeURIComponent(fallbackName)}`;
|
|
1184
|
+
}
|
|
1185
|
+
|
|
1186
|
+
const WORKSPACE_FILE_SKIP_REL_PREFIXES = [
|
|
1187
|
+
".workspace/agentflow/worktrees",
|
|
1188
|
+
".workspace/agentflow/git-repos",
|
|
1189
|
+
".workspace/agentflow/runBuild",
|
|
1190
|
+
".workspace/agentflow/composer-logs",
|
|
1191
|
+
];
|
|
1192
|
+
|
|
1193
|
+
function workspacePathInside(parent, candidate) {
|
|
1194
|
+
const base = path.resolve(parent);
|
|
1195
|
+
const target = path.resolve(candidate);
|
|
1196
|
+
return target === base || target.startsWith(base + path.sep);
|
|
1197
|
+
}
|
|
1198
|
+
|
|
1199
|
+
function shouldSkipWorkspaceFileRelPath(relPath) {
|
|
1200
|
+
const normalized = String(relPath || "").replace(/\\/g, "/").replace(/^\/+/, "");
|
|
1201
|
+
return WORKSPACE_FILE_SKIP_REL_PREFIXES.some((prefix) => (
|
|
1202
|
+
normalized === prefix || normalized.startsWith(`${prefix}/`)
|
|
1203
|
+
));
|
|
1204
|
+
}
|
|
1205
|
+
|
|
1180
1206
|
function readWorkspaceFilesRecursive(dir, root, depth = 0, maxDepth = 3, budget = { count: 0 }) {
|
|
1181
1207
|
if (depth > maxDepth || budget.count > 500) return [];
|
|
1182
1208
|
let entries;
|
|
@@ -1191,6 +1217,7 @@ function readWorkspaceFilesRecursive(dir, root, depth = 0, maxDepth = 3, budget
|
|
|
1191
1217
|
if (entry.name.startsWith(".") && entry.name !== ".agents" && entry.name !== ".codex") continue;
|
|
1192
1218
|
const abs = path.join(dir, entry.name);
|
|
1193
1219
|
const rel = path.relative(root, abs).replace(/\\/g, "/");
|
|
1220
|
+
if (shouldSkipWorkspaceFileRelPath(rel)) continue;
|
|
1194
1221
|
if (entry.isDirectory()) {
|
|
1195
1222
|
if (WORKSPACE_FILE_SKIP_DIRS.has(entry.name)) continue;
|
|
1196
1223
|
budget.count++;
|
|
@@ -1262,6 +1289,43 @@ function normalizeWorkspaceGraphPayload(payload) {
|
|
|
1262
1289
|
};
|
|
1263
1290
|
}
|
|
1264
1291
|
|
|
1292
|
+
function workspaceRunTouchedNodeIds(result) {
|
|
1293
|
+
const ids = new Set();
|
|
1294
|
+
for (const id of Array.isArray(result?.order) ? result.order : []) {
|
|
1295
|
+
const text = String(id || "").trim();
|
|
1296
|
+
if (text) ids.add(text);
|
|
1297
|
+
}
|
|
1298
|
+
for (const event of Array.isArray(result?.events) ? result.events : []) {
|
|
1299
|
+
const nodeId = String(event?.nodeId || "").trim();
|
|
1300
|
+
if (nodeId) ids.add(nodeId);
|
|
1301
|
+
for (const displayId of Array.isArray(event?.displayNodeIds) ? event.displayNodeIds : []) {
|
|
1302
|
+
const text = String(displayId || "").trim();
|
|
1303
|
+
if (text) ids.add(text);
|
|
1304
|
+
}
|
|
1305
|
+
}
|
|
1306
|
+
return ids;
|
|
1307
|
+
}
|
|
1308
|
+
|
|
1309
|
+
function mergeWorkspaceRunGraph(currentGraph, runGraph, touchedIds) {
|
|
1310
|
+
const current = normalizeWorkspaceGraphPayload(currentGraph || {});
|
|
1311
|
+
const run = normalizeWorkspaceGraphPayload(runGraph || {});
|
|
1312
|
+
const ids = touchedIds instanceof Set ? touchedIds : new Set(touchedIds || []);
|
|
1313
|
+
const instances = { ...(current.instances || {}) };
|
|
1314
|
+
for (const id of ids) {
|
|
1315
|
+
if (run.instances && Object.prototype.hasOwnProperty.call(run.instances, id)) {
|
|
1316
|
+
instances[id] = run.instances[id];
|
|
1317
|
+
}
|
|
1318
|
+
}
|
|
1319
|
+
return {
|
|
1320
|
+
...current,
|
|
1321
|
+
version: 1,
|
|
1322
|
+
instances,
|
|
1323
|
+
edges: Array.isArray(current.edges) ? current.edges : [],
|
|
1324
|
+
ui: current.ui && typeof current.ui === "object" ? current.ui : { nodePositions: {} },
|
|
1325
|
+
updatedAt: new Date().toISOString(),
|
|
1326
|
+
};
|
|
1327
|
+
}
|
|
1328
|
+
|
|
1265
1329
|
function resolveWorkspaceScopeRoot(workspaceRoot, params = {}, opts = {}) {
|
|
1266
1330
|
const flowId = params.flowId != null ? String(params.flowId).trim() : "";
|
|
1267
1331
|
if (!flowId) return { root: path.resolve(workspaceRoot), flowId: "", flowSource: "", archived: false };
|
|
@@ -1982,6 +2046,39 @@ function workspaceTaskUpstreamText(graph, nodeId, outputs) {
|
|
|
1982
2046
|
return workspaceOutputSlotValueForEdge(graph, outputs, contentEdge);
|
|
1983
2047
|
}
|
|
1984
2048
|
|
|
2049
|
+
function workspaceInputValues(graph, nodeId, outputs) {
|
|
2050
|
+
const values = {};
|
|
2051
|
+
const edges = Array.isArray(graph?.edges) ? graph.edges : [];
|
|
2052
|
+
const instances = graph?.instances && typeof graph.instances === "object" ? graph.instances : {};
|
|
2053
|
+
const target = instances[String(nodeId || "")] || {};
|
|
2054
|
+
const inputSlots = Array.isArray(target.input) ? target.input : [];
|
|
2055
|
+
for (const edge of edges) {
|
|
2056
|
+
if (String(edge?.target || "") !== String(nodeId)) continue;
|
|
2057
|
+
const index = workspaceHandleIndex(edge?.targetHandle, "input");
|
|
2058
|
+
const slot = inputSlots[index] || null;
|
|
2059
|
+
const name = String(slot?.name || "").trim();
|
|
2060
|
+
if (!name || isWorkspaceSemanticInputSlot(slot)) continue;
|
|
2061
|
+
const value = workspaceOutputSlotValueForEdge(graph, outputs, edge);
|
|
2062
|
+
if (String(value || "").trim()) values[name] = String(value);
|
|
2063
|
+
}
|
|
2064
|
+
for (const slot of inputSlots) {
|
|
2065
|
+
const name = String(slot?.name || "").trim();
|
|
2066
|
+
if (!name || isWorkspaceSemanticInputSlot(slot) || Object.prototype.hasOwnProperty.call(values, name)) continue;
|
|
2067
|
+
const value = workspaceSlotValue(slot);
|
|
2068
|
+
if (String(value || "").trim()) values[name] = String(value);
|
|
2069
|
+
}
|
|
2070
|
+
return values;
|
|
2071
|
+
}
|
|
2072
|
+
|
|
2073
|
+
function workspaceResolveBodyPlaceholders(body, inputValues = {}) {
|
|
2074
|
+
const raw = String(body || "");
|
|
2075
|
+
if (!raw.includes("${")) return raw;
|
|
2076
|
+
return raw.replace(/\$\{([A-Za-z_][A-Za-z0-9_-]*)\}/g, (match, name) => {
|
|
2077
|
+
if (!Object.prototype.hasOwnProperty.call(inputValues, name)) return match;
|
|
2078
|
+
return String(inputValues[name] ?? "");
|
|
2079
|
+
});
|
|
2080
|
+
}
|
|
2081
|
+
|
|
1985
2082
|
function parseWorkspaceSkillKeys(raw) {
|
|
1986
2083
|
const text = String(raw || "").trim();
|
|
1987
2084
|
if (!text) return [];
|
|
@@ -2146,9 +2243,9 @@ function workspaceUpdateDirectDisplays(graph, sourceId, content, outputs = null)
|
|
|
2146
2243
|
return updated;
|
|
2147
2244
|
}
|
|
2148
2245
|
|
|
2149
|
-
function workspaceNodePrompt(graph, nodeId, upstreamText, skillsBlock, mcpBlock = "") {
|
|
2246
|
+
function workspaceNodePrompt(graph, nodeId, upstreamText, skillsBlock, mcpBlock = "", inputValues = {}) {
|
|
2150
2247
|
const instance = graph.instances[nodeId] || {};
|
|
2151
|
-
const body =
|
|
2248
|
+
const body = workspaceResolveBodyPlaceholders(instance.body || "", inputValues).trim();
|
|
2152
2249
|
const label = String(instance.label || nodeId).trim();
|
|
2153
2250
|
const downstreamRequirements = workspaceDownstreamDisplayRequirements(graph, nodeId);
|
|
2154
2251
|
const outputProtocolRequirements = workspaceOutputProtocolRequirements(graph, nodeId);
|
|
@@ -2166,6 +2263,72 @@ function workspaceNodePrompt(graph, nodeId, upstreamText, skillsBlock, mcpBlock
|
|
|
2166
2263
|
].filter(Boolean).join("\n");
|
|
2167
2264
|
}
|
|
2168
2265
|
|
|
2266
|
+
function workspaceDefaultWorktreeRoot(scopedRoot) {
|
|
2267
|
+
return path.join(path.resolve(scopedRoot), ".workspace", "agentflow", "worktrees");
|
|
2268
|
+
}
|
|
2269
|
+
|
|
2270
|
+
function workspaceShouldAutoCleanupWorktree(scopedRoot, worktreePath, hasExplicitWorktreePath) {
|
|
2271
|
+
if (hasExplicitWorktreePath || !worktreePath) return false;
|
|
2272
|
+
return workspacePathInside(workspaceDefaultWorktreeRoot(scopedRoot), worktreePath);
|
|
2273
|
+
}
|
|
2274
|
+
|
|
2275
|
+
function workspaceTrackAutoCleanupWorktree(list, item) {
|
|
2276
|
+
const rawTarget = String(item?.worktreePath || "").trim();
|
|
2277
|
+
if (!rawTarget) return;
|
|
2278
|
+
const target = path.resolve(rawTarget);
|
|
2279
|
+
if (list.some((entry) => path.resolve(entry.worktreePath) === target)) return;
|
|
2280
|
+
list.push({ ...item, worktreePath: target });
|
|
2281
|
+
}
|
|
2282
|
+
|
|
2283
|
+
function workspaceUntrackAutoCleanupWorktree(list, worktreePath) {
|
|
2284
|
+
const rawTarget = String(worktreePath || "").trim();
|
|
2285
|
+
if (!rawTarget) return;
|
|
2286
|
+
const target = path.resolve(rawTarget);
|
|
2287
|
+
for (let i = list.length - 1; i >= 0; i -= 1) {
|
|
2288
|
+
if (path.resolve(list[i].worktreePath) === target) list.splice(i, 1);
|
|
2289
|
+
}
|
|
2290
|
+
}
|
|
2291
|
+
|
|
2292
|
+
function workspaceMarkAutoWorktreeCleaned(graph, entry) {
|
|
2293
|
+
const instance = graph?.instances?.[entry.nodeId];
|
|
2294
|
+
if (!instance) return false;
|
|
2295
|
+
let nextInstance = workspaceSetOutputSlot(instance, "worktreePath", "");
|
|
2296
|
+
nextInstance = workspaceSetOutputSlot(nextInstance, "gitContext", "");
|
|
2297
|
+
nextInstance = workspaceSetOutputSlot(nextInstance, "workspaceContext", "");
|
|
2298
|
+
graph.instances[entry.nodeId] = nextInstance;
|
|
2299
|
+
return true;
|
|
2300
|
+
}
|
|
2301
|
+
|
|
2302
|
+
function workspaceCleanupAutoWorktrees(list, graph, emit) {
|
|
2303
|
+
for (const entry of [...list].reverse()) {
|
|
2304
|
+
try {
|
|
2305
|
+
const result = unloadGitWorktree({
|
|
2306
|
+
repoPath: entry.repoPath,
|
|
2307
|
+
worktreePath: entry.worktreePath,
|
|
2308
|
+
force: false,
|
|
2309
|
+
prune: true,
|
|
2310
|
+
});
|
|
2311
|
+
emit({
|
|
2312
|
+
type: "natural",
|
|
2313
|
+
kind: "status",
|
|
2314
|
+
nodeId: entry.nodeId,
|
|
2315
|
+
text: `已清理临时 worktree:${result.worktreePath}`,
|
|
2316
|
+
});
|
|
2317
|
+
if (workspaceMarkAutoWorktreeCleaned(graph, entry)) {
|
|
2318
|
+
emit({ type: "graph", nodeId: entry.nodeId, graph });
|
|
2319
|
+
}
|
|
2320
|
+
} catch (e) {
|
|
2321
|
+
emit({
|
|
2322
|
+
type: "natural",
|
|
2323
|
+
kind: "warning",
|
|
2324
|
+
nodeId: entry.nodeId,
|
|
2325
|
+
text: `临时 worktree 未自动清理:${entry.worktreePath}\n原因:${e?.message || String(e)}`,
|
|
2326
|
+
});
|
|
2327
|
+
}
|
|
2328
|
+
}
|
|
2329
|
+
list.splice(0, list.length);
|
|
2330
|
+
}
|
|
2331
|
+
|
|
2169
2332
|
async function runWorkspaceGraph(root, scopedRoot, payload, userCtx = {}, opts = {}) {
|
|
2170
2333
|
const graph = normalizeWorkspaceGraphPayload(payload.graph || {});
|
|
2171
2334
|
const runNodeId = String(payload?.runNodeId || "").trim();
|
|
@@ -2223,7 +2386,9 @@ async function runWorkspaceGraph(root, scopedRoot, payload, userCtx = {}, opts =
|
|
|
2223
2386
|
};
|
|
2224
2387
|
let cwd = scopedRoot;
|
|
2225
2388
|
const modelKey = typeof payload?.model === "string" ? payload.model.trim() : "";
|
|
2389
|
+
const autoCleanupWorktrees = [];
|
|
2226
2390
|
|
|
2391
|
+
try {
|
|
2227
2392
|
for (const nodeId of order) {
|
|
2228
2393
|
throwIfAborted();
|
|
2229
2394
|
const instance = graph.instances[nodeId];
|
|
@@ -2408,13 +2573,23 @@ async function runWorkspaceGraph(root, scopedRoot, payload, userCtx = {}, opts =
|
|
|
2408
2573
|
(gitContext?.repoPath ? path.resolve(gitContext.repoPath) : "");
|
|
2409
2574
|
if (!repoPath) throw new Error("Load Worktree requires repoPath");
|
|
2410
2575
|
const branch = workspaceSlotValue(workspaceSlotByName(instance, "branch")).trim();
|
|
2411
|
-
const
|
|
2576
|
+
const worktreeInputSlot = (Array.isArray(instance.input) ? instance.input : [])
|
|
2577
|
+
.find((slot) => String(slot?.name || "") === "worktreePath") || null;
|
|
2578
|
+
const rawWorktreePath = workspaceSlotValue(worktreeInputSlot || workspaceSlotByName(instance, "worktreePath")).trim();
|
|
2412
2579
|
const worktreePath = rawWorktreePath ? workspaceResolvePath(cwd, rawWorktreePath) : (gitContext?.worktreePath ? path.resolve(gitContext.worktreePath) : "");
|
|
2580
|
+
const hasExplicitWorktreePath = Boolean(rawWorktreePath) || Boolean(gitContext?.worktreePath);
|
|
2413
2581
|
const previousCwd = cwd;
|
|
2414
2582
|
const force = ["true", "1", "yes", "on"].includes(workspaceSlotValue(workspaceSlotByName(instance, "force")).trim().toLowerCase());
|
|
2415
2583
|
const pruneMissingRaw = workspaceSlotValue(workspaceSlotByName(instance, "pruneMissing")).trim().toLowerCase();
|
|
2416
2584
|
const pruneMissing = pruneMissingRaw !== "false";
|
|
2417
2585
|
const result = loadGitWorktree({ repoPath, branch, worktreePath, pipelineWorkspace: scopedRoot, force, pruneMissing });
|
|
2586
|
+
if (workspaceShouldAutoCleanupWorktree(scopedRoot, result.worktreePath, hasExplicitWorktreePath)) {
|
|
2587
|
+
workspaceTrackAutoCleanupWorktree(autoCleanupWorktrees, {
|
|
2588
|
+
nodeId,
|
|
2589
|
+
repoPath: result.repoRoot,
|
|
2590
|
+
worktreePath: result.worktreePath,
|
|
2591
|
+
});
|
|
2592
|
+
}
|
|
2418
2593
|
const outGitContext = buildGitContext({
|
|
2419
2594
|
repoPath: result.repoRoot,
|
|
2420
2595
|
worktreePath: result.worktreePath,
|
|
@@ -2457,6 +2632,7 @@ async function runWorkspaceGraph(root, scopedRoot, payload, userCtx = {}, opts =
|
|
|
2457
2632
|
const pruneRaw = workspaceSlotValue(workspaceSlotByName(instance, "prune")).trim().toLowerCase();
|
|
2458
2633
|
const prune = pruneRaw !== "false";
|
|
2459
2634
|
const result = unloadGitWorktree({ repoPath, worktreePath, force, prune });
|
|
2635
|
+
workspaceUntrackAutoCleanupWorktree(autoCleanupWorktrees, result.worktreePath);
|
|
2460
2636
|
const previousContext = workspaceContext?.previous && typeof workspaceContext.previous === "object" ? workspaceContext.previous : null;
|
|
2461
2637
|
cwd = previousContext?.cwd ? path.resolve(String(previousContext.cwd)) : scopedRoot;
|
|
2462
2638
|
let nextInstance = workspaceSetOutputSlot(instance, "removed", "true");
|
|
@@ -2513,14 +2689,15 @@ async function runWorkspaceGraph(root, scopedRoot, payload, userCtx = {}, opts =
|
|
|
2513
2689
|
|
|
2514
2690
|
const prepareStartedAt = Date.now();
|
|
2515
2691
|
const upstreamText = workspaceTaskUpstreamText(graph, nodeId, outputs);
|
|
2516
|
-
const
|
|
2692
|
+
const inputValues = workspaceInputValues(graph, nodeId, outputs);
|
|
2693
|
+
const body = workspaceResolveBodyPlaceholders(instance.body || "", inputValues).trim();
|
|
2517
2694
|
if (defId === "agent_subAgent" && !body && !String(upstreamText || "").trim()) {
|
|
2518
2695
|
throw new Error(`Workspace node ${nodeId} has no task. Fill the node body or connect upstream text.`);
|
|
2519
2696
|
}
|
|
2520
2697
|
const upstreamSkillBlocks = workspaceUpstreamSkillBlocks(graph, nodeId, outputs);
|
|
2521
2698
|
const promptSkillsBlock = mergeWorkspaceSkillBlocks(upstreamSkillBlocks, upstreamSkillBlocks ? "" : loadSkillsBlockForKeys(fallbackSelectedSkillKeys));
|
|
2522
2699
|
const promptMcpBlock = workspaceUpstreamMcpBlocks(graph, nodeId, outputs);
|
|
2523
|
-
const prompt = workspaceNodePrompt(graph, nodeId, upstreamText, promptSkillsBlock, promptMcpBlock);
|
|
2700
|
+
const prompt = workspaceNodePrompt(graph, nodeId, upstreamText, promptSkillsBlock, promptMcpBlock, inputValues);
|
|
2524
2701
|
emitTiming(nodeId, "prepare-agent-prompt", prepareStartedAt, { promptChars: prompt.length, upstreamChars: String(upstreamText || "").length, skillsChars: promptSkillsBlock.length, mcpChars: promptMcpBlock.length });
|
|
2525
2702
|
emit({ type: "natural", kind: "prompt", nodeId, text: prompt });
|
|
2526
2703
|
let content = "";
|
|
@@ -2581,6 +2758,9 @@ async function runWorkspaceGraph(root, scopedRoot, payload, userCtx = {}, opts =
|
|
|
2581
2758
|
if (slotUpdate.changed || updatedDisplays.length) emit({ type: "graph", nodeId, displayNodeIds: updatedDisplays, graph });
|
|
2582
2759
|
emit({ type: "node-done", nodeId, definitionId: defId });
|
|
2583
2760
|
}
|
|
2761
|
+
} finally {
|
|
2762
|
+
workspaceCleanupAutoWorktrees(autoCleanupWorktrees, graph, emit);
|
|
2763
|
+
}
|
|
2584
2764
|
if (pauseNodeIds.length > 0) {
|
|
2585
2765
|
emit({ type: "paused", nodeIds: pauseNodeIds, message: `Workspace run paused at ${pauseNodeIds.join(", ")}` });
|
|
2586
2766
|
}
|
|
@@ -3402,8 +3582,11 @@ export function startUiServer({
|
|
|
3402
3582
|
signal: controller.signal,
|
|
3403
3583
|
onActiveChild: setActiveChild,
|
|
3404
3584
|
});
|
|
3405
|
-
|
|
3406
|
-
|
|
3585
|
+
const currentGraph = readWorkspaceGraph(scoped.root).graph;
|
|
3586
|
+
const touchedIds = workspaceRunTouchedNodeIds(result);
|
|
3587
|
+
const mergedGraph = mergeWorkspaceRunGraph(currentGraph, result.graph, touchedIds);
|
|
3588
|
+
fs.writeFileSync(graphPath, JSON.stringify(mergedGraph, null, 2) + "\n", "utf-8");
|
|
3589
|
+
writeEvent({ type: "done", ok: true, path: graphPath, graph: mergedGraph, order: result.order, touchedNodeIds: Array.from(touchedIds), pauseNodeIds: result.pauseNodeIds || [] });
|
|
3407
3590
|
res.end();
|
|
3408
3591
|
} catch (e) {
|
|
3409
3592
|
if (isWorkspaceRunAbortError(e) || controller.signal.aborted) {
|
|
@@ -3423,8 +3606,11 @@ export function startUiServer({
|
|
|
3423
3606
|
onActiveChild: setActiveChild,
|
|
3424
3607
|
});
|
|
3425
3608
|
const graphPath = workspaceGraphPath(scoped.root);
|
|
3426
|
-
|
|
3427
|
-
|
|
3609
|
+
const currentGraph = readWorkspaceGraph(scoped.root).graph;
|
|
3610
|
+
const touchedIds = workspaceRunTouchedNodeIds(result);
|
|
3611
|
+
const mergedGraph = mergeWorkspaceRunGraph(currentGraph, result.graph, touchedIds);
|
|
3612
|
+
fs.writeFileSync(graphPath, JSON.stringify(mergedGraph, null, 2) + "\n", "utf-8");
|
|
3613
|
+
json(res, 200, { ok: true, path: graphPath, ...result, graph: mergedGraph, touchedNodeIds: Array.from(touchedIds) });
|
|
3428
3614
|
} catch (e) {
|
|
3429
3615
|
if (isWorkspaceRunAbortError(e) || controller.signal.aborted) {
|
|
3430
3616
|
json(res, 200, { ok: false, stopped: true, message: "Workspace run stopped" });
|
|
@@ -3504,7 +3690,7 @@ export function startUiServer({
|
|
|
3504
3690
|
json(res, 400, { error: scoped.error });
|
|
3505
3691
|
return;
|
|
3506
3692
|
}
|
|
3507
|
-
const { abs } = resolveWorkspaceFilePath(scoped.root, url.searchParams.get("path") || "");
|
|
3693
|
+
const { abs, rel } = resolveWorkspaceFilePath(scoped.root, url.searchParams.get("path") || "");
|
|
3508
3694
|
if (!fs.existsSync(abs) || !fs.statSync(abs).isFile()) {
|
|
3509
3695
|
json(res, 404, { error: "File not found" });
|
|
3510
3696
|
return;
|
|
@@ -3512,11 +3698,15 @@ export function startUiServer({
|
|
|
3512
3698
|
const ext = path.extname(abs).toLowerCase();
|
|
3513
3699
|
const type = MIME[ext] || "application/octet-stream";
|
|
3514
3700
|
const data = fs.readFileSync(abs);
|
|
3515
|
-
|
|
3701
|
+
const headers = {
|
|
3516
3702
|
"Content-Type": type,
|
|
3517
3703
|
"Content-Length": data.length,
|
|
3518
3704
|
"Cache-Control": "no-store",
|
|
3519
|
-
}
|
|
3705
|
+
};
|
|
3706
|
+
if (url.searchParams.get("download") === "1") {
|
|
3707
|
+
headers["Content-Disposition"] = workspaceDownloadContentDisposition(rel);
|
|
3708
|
+
}
|
|
3709
|
+
res.writeHead(200, headers);
|
|
3520
3710
|
res.end(data);
|
|
3521
3711
|
} catch (e) {
|
|
3522
3712
|
json(res, /traversal/i.test(String(e.message || e)) ? 403 : 500, { error: (e && e.message) || String(e) });
|